/*******************************************************\
|* Author: Trevor Sundberg
\*******************************************************/

// Header protection
#pragma once
#ifndef SKUGO_BUFFER_HPP
#define SKUGO_BUFFER_HPP

// Includes
#include "SkugoTypes.hpp"
#include <vector>
#include <string>

namespace Skugo
{
  // Defines
  #define SkugoSizeOf(type)                                       SizeTemplate <type>()()
  #define SkugoSizeOfReference(type, referenceForm)               SizeTemplate <type>()(referenceForm)
  #define SkugoRead(type, buffer)                                 ReadTemplate <type>()(buffer)
  #define SkugoWrite(type, value, buffer)                         WriteTemplate<type>()(value, buffer)
  #define SkugoWriteReference(type, value, buffer, referenceForm) WriteTemplate<type>()(value, buffer, referenceForm)

  // Using directives
  using std::vector;
  using std::string;

  // When writing a reference, we need to know whether its being written
  // as a pointer or if it's value should be written to the buffer
  // (this does mean that references actually add extra data to the buffer)
  namespace ReferenceForm
  {
    enum Enum
    {
      AsPointer = 0,
      AsValue   = 1
    };
  }



  // Get the size of a given type
  template <typename T>
  struct SizeTemplate
  {
    sizeType operator ()()
    {
      return sizeof(T);
    }
  };

  // Get the size of a given reference type
  template <typename T>
  struct SizeTemplate<T&>
  {
    sizeType operator ()(ReferenceForm::Enum referenceForm)
    {
      // If we want the size as a pointer...
      if (referenceForm == ReferenceForm::AsPointer)
      {
        // Add one for the size of the ReferenceForm enum
        return sizeof(T*) + 1;
      }
      // If we want the size as a value...
      else
      {
        // Add one for the size of the ReferenceForm enum
        return sizeof(T) + 1;
      }
    }
  };

  // Read a type from a buffer, and push the buffer forward
  template <typename T>
  struct ReadTemplate
  {
    // Read a type from a buffer, and push the buffer forward
    // The buffer end is provided simply as a safety check, and can be NULL
    T& operator ()(char*& buffer)
    {
      // Cast the buffer into the type that we wish to read
      T& value = *reinterpret_cast<T*>(buffer);
      
      // Move the buffer forward
      buffer += sizeof(T);
      
      // Return the reference to the value we just read
      return value;
    }
  };

  // Read a reference type from a buffer, and push the buffer forward
  template <typename T>
  struct ReadTemplate<T&>
  {
    T& operator ()(char*& buffer)
    {
      // Read the first byte (tells us which reference form was chosen)
      ReferenceForm::Enum referenceForm = (ReferenceForm::Enum)*buffer;

      // Move the buffer forward
      ++buffer;

      // If the reference was written as a pointer...
      if (referenceForm == ReferenceForm::AsPointer)
      {
        // Cast the buffer into the type that we wish to read
        T* value = *reinterpret_cast<T**>(buffer);
      
        // Move the buffer forward
        buffer += sizeof(T*);
      
        // Return the reference to the value we just read
        return *value;
      }
      // If the reference was written as a value...
      else
      {
        // Cast the buffer into the type that we wish to read
        T& value = *reinterpret_cast<T*>(buffer);
      
        // Move the buffer forward
        buffer += sizeof(T);
      
        // Return the reference to the value we just read
        return value;
      }
    }
  };

  // Write a type to a buffer, and push the buffer forward
  template <typename T>
  struct WriteTemplate
  {
    void operator ()(const T& value, char*& output)
    {
      // Write to the type by reinterpreting the character buffer
      *reinterpret_cast<T*>(output) = value;
      
      // Move the buffer forward
      output += sizeof(T);
    }
  };

  // Write a reference type to a buffer, and push the buffer forward
  template <typename T>
  struct WriteTemplate<T&>
  {
    void operator ()(const T& value, char*& output)
    {
      operator()(value, output, ReferenceForm::AsPointer);
    }

    void operator ()(const T& value, char*& output, ReferenceForm::Enum referenceForm)
    {
      // Write the first byte (tells them which reference form was chosen)
      *output = (char)referenceForm;

      // Move the buffer forward
      ++output;

      // If we wish to write the reference as a pointer...
      if (referenceForm == ReferenceForm::AsPointer)
      {
        // Write to the type by reinterpreting the character buffer
        *reinterpret_cast<const T**>(output) = &value;
        
        // Move the buffer forward
        output += sizeof(T*);
      }
      // If we wish to write the reference as a value...
      else
      {
        // Write to the type by reinterpreting the character buffer
        *reinterpret_cast<T*>(output) = value;
      
        // Move the buffer forward
        output += sizeof(T);
      }
    }
  };

  // Facilitates reading and writing to a buffer
  // The buffer can be stored internally, or it can be passed in
  class Buffer
  {
  public:

    // Constructor
    Buffer();

    // Constructor that makes the buffer read and write to the given data
    Buffer(const char* data, sizeType size);

    // Get the size of the buffer (same as the position)
    sizeType GetSize() const;

    // Get the position of the buffer (same as the size)
    sizeType GetPosition() const;

    // Set the position manually
    void SetPosition(sizeType position);

    // Reset the position
    void ResetPosition();

    // Get the data from the buffer
    char* GetData() const;
    
    // Write raw data to the buffer
    void Write(const void* data, sizeType size);

    // Read raw data from the buffer
    void Read(void* dataOut, sizeType size);

    // Write a type to the buffer.
    template <typename T>
    void Write(const T& value)
    {
      // Resize the data
      AddSize(SkugoSizeOf(T));

      // Get the original position
      char* start = GetData() + mPosition;

      // Write the data to the buffer
      char* data = start;
      SkugoWrite(T, value, data);

      // Move our position forward by the amount of data written
      mPosition += (data - start);
    }

    // Read a string as an ascii string to a binary reader.
    string ReadAsciiString()
    {
      auto length = this->Read<unsigned int>();
      auto data = (char*)alloca(length);
      this->Read(data, length);
      return string(data, length);
    }

    // Write a string as an ascii string to a binary writer.
    void WriteAsciiString(const string& value)
    {
      auto length = value.size();
      this->Write<unsigned int>(length);
      this->Write(value.c_str(), length);
    }

    // Write a type to the buffer
    template <typename T>
    void WriteReference(const T& value, ReferenceForm::Enum referenceForm)
    {
      // Resize the data to fit any reference type (+1 for the enum)
      AddSize(SkugoSizeOfReference(T, referenceForm));

      // Get the original position
      char* start = GetData() + mPosition;

      // Write the data to the buffer
      char* data = start;
      SkugoWriteReference(T&, value, data, referenceForm);

      // Move our position forward by the amount of data written
      mPosition += (data - start);
    }

    // Write a type to the buffer
    template <typename T>
    T& Read()
    {
      // We need to do some error checking here...

      // Get the original position
      char* start = GetData() + mPosition;

      // Read the data from the buffer
      char* data = start;
      T& value = SkugoRead(T, data);

      // Move our position forward by the amount of data read
      mPosition += (data - start);

      // Return the reference
      return value;
    }

  private:

    // Add to the size of the buffer
    void AddSize(sizeType amount);

  private:

    // The current position in the buffer
    sizeType mPosition;

    // If the user passed in a data buffer, then we read/write to that instead
    char* mUserData;

    // The size of the user-data (if we are using user-data)
    sizeType mUserDataSize;

    // A resizable data array
    vector<char> mData;
  };
}

// End header protection
#endif
